/**********************************************************************
 * SailmiPIM - modular cross-platform feature Personal Information Manager Tools.
 * Copyright(C) 2006-2014  Georg Rudoy
 *
 * Distributed under the Boost Software License, Version 1.0.
 *(See accompanying file LICENSE or copy at https://www.boost.org/LICENSE_1_0.txt)
 **********************************************************************/

#include "entitymanager.h"
#include <algorithm>
#include <QThread>
#include <QFuture>
#include <QDesktopServices>
#include <QUrl>
#include <QTextCodec>
#include "util/core/util.h"
#include "sailmipim/structures.h"
#include "sailmipim/idownload.h"
#include "sailmipim/ientityhandler.h"
#include "sailmipim/entitytesthandleresult.h"
#include "util/xpc/ef/entityfields.h"
#include "core.h"
#include "pluginmanager.h"
#include "xsd/xmlsettingsmanager.h"
#include "handlerchoicedialog.h"

namespace SailmiPIM
{
	EntityManager::EntityManager(QObject *plugin, QObject *parent)
	: QObject { parent }
	, Plugin_ { plugin }
	{
	}

	namespace
	{
		template<typename T, typename F>
		QObjectList GetSubtype(const Entity& e, F&& queryFunc)
		{
			auto pm = Core::Instance().GetPluginManager();
			QMap<int, QObjectList> result;
			for(const auto& plugin : pm->GetAllCastableRoots<T>())
			{
				EntityTestHandleResult r;
				try
				{
					r = queryFunc(e, qobject_cast<T>(plugin));
					if(r.HandlePriority_ > 0)
						result [r.HandlePriority_] << plugin;
				}
				catch(const std::exception& e)
				{
					qWarning() << Q_FUNC_INFO
						<< "could not query"
						<< e.what()
						<< plugin;
				}
				catch(...)
				{
					qWarning() << Q_FUNC_INFO
						<< "could not query"
						<< plugin;
				}
			}

			if(result.isEmpty())
				return {};
			return result.last();
		}

		QObjectList GetObjects(QObject *self, const Entity& e, int *downloaders = 0, int *handlers = 0)
		{
			if(Core::Instance().IsShuttingDown())
				return {};

			auto removeUnwanted = [&](QObjectList& handlers)
			{
				if(e.Additional_ [IgnoreSelf].toBool())
					handlers.removeOne(self);
			};

			QObjectList result;
			if(!(e.Parameters_ & TaskParameter::OnlyHandle))
			{
				auto sub = GetSubtype<IDownload*>(e,
						[](const Entity& e, IDownload *dl) { return dl->CouldDownload(e); });
				removeUnwanted(sub);
				if(downloaders)
					*downloaders = sub.size();
				result += sub;
			}
			if(!(e.Parameters_ & TaskParameter::OnlyDownload))
			{
				auto sub = GetSubtype<IEntityHandler*>(e,
						[](const Entity& e, IEntityHandler *eh) { return eh->CouldHandle(e); });
				removeUnwanted(sub);
				if(handlers)
					*handlers = sub.size();
				result += sub;
			}
			return result;
		}

		bool NoHandlersAvailable(const Entity& e)
		{
			if(!(e.Parameters_ & FromUserInitiated) ||
					(e.Parameters_ & OnlyDownload))
				return false;

			if(!XmlSettingsManager::Instance()->
						property("FallbackExternalHandlers").toBool())
				return false;

			const auto& url = e.Entity_.toUrl();
			if(url.scheme().isEmpty())
				return false;

			if(e.Parameters_ & FromCommandLine)
			{
				qWarning() << Q_FUNC_INFO
						<< "refusing to pass again "
						<< url
						<< "to the environment as it was added from there";
				return false;
			}

			if(e.Parameters_ & IsDownloaded)
			{
				qDebug() << Q_FUNC_INFO
						<< "avoiding opening"
						<< url
						<< "by external apps as it is just downloaded from teh internets";
				return false;
			}

			QDesktopServices::openUrl(url);
			return true;
		}

		QString GetUserText(const Entity& p)
		{
			QString string = QObject::tr("Too long to show");
			if(p.Additional_.contains("UserVisibleName") &&
					p.Additional_ ["UserVisibleName"].canConvert<QString>())
				string = p.Additional_ ["UserVisibleName"].toString();
			else if(p.Entity_.canConvert<QByteArray>())
			{
				QByteArray entity = p.Entity_.toByteArray();
				if(entity.size() < 100)
					string = QTextCodec::codecForName("UTF-8")->toUnicode(entity);
			}
			else if(p.Entity_.canConvert<QUrl>())
			{
				string = p.Entity_.toUrl().toString();
				if(string.size() > 100)
					string = string.left(97) + "...";
			}
			else
				string = QObject::tr("Binary entity");

			if(!p.Mime_.isEmpty())
				string += QObject::tr("<br /><br />of type <code>%1</code>").arg(p.Mime_);

			if(!p.Additional_ ["SourceURL"].toUrl().isEmpty())
			{
				QString urlStr = p.Additional_ ["SourceURL"].toUrl().toString();
				if(urlStr.size() > 63)
					urlStr = urlStr.left(60) + "...";
				string += QObject::tr("<br />from %1")
						.arg(urlStr);
			}

			return string;
		}


		bool GetPreparedObjectList(QObject *self, Entity& e, QObject *desired, QObjectList& handlers, bool handling)
		{
			int numDownloaders = 0, numHandlers = 0;
			if(desired)
				handlers << desired;
			else
				handlers = GetObjects(self, e, &numDownloaders, &numHandlers);

			if(handlers.isEmpty() && !desired)
				return handling ? NoHandlersAvailable(e) : false;

			bool shouldAsk = false;
			if(e.Parameters_ & FromUserInitiated && !(e.Parameters_ & AutoAccept))
			{
				const bool askHandlers = XmlSettingsManager::Instance()->property("DontAskWhenSingle").toBool() ?
						numHandlers > 1 :
						numHandlers;
				const bool askDownloaders =(e.Parameters_ & IsDownloaded) ?
						numDownloaders > 1 :
						numDownloaders;
				shouldAsk = askHandlers || askDownloaders ||(numHandlers && numDownloaders);
			}

			if(shouldAsk)
			{
				HandlerChoiceDialog dia(GetUserText(e));
				for(auto handler : handlers.mid(0, numDownloaders))
					dia.Add(qobject_cast<IInfo*>(handler), qobject_cast<IDownload*>(handler));
				for(auto handler : handlers.mid(numDownloaders, numHandlers))
					dia.Add(qobject_cast<IInfo*>(handler), qobject_cast<IEntityHandler*>(handler));
				dia.SetFilenameSuggestion(e.Location_);
				if(dia.exec() != QDialog::Accepted || !dia.GetSelected())
					return false;

				auto selected = dia.GetSelected();
				if(qobject_cast<IDownload*>(selected))
				{
					const QString& dir = dia.GetFilename();
					if(dir.isEmpty())
						return false;
					e.Location_ = dir;
				}

				handlers.clear();
				handlers << selected;
			}

			return true;
		}
	}

	IEntityManager::DelegationResult EntityManager::DelegateEntity(Entity e, QObject *desired)
	{
		if(!CheckInitStage(e, desired, DelegateAfterInit_))
			return {};

		e.Parameters_ |= OnlyDownload;
		QObjectList handlers;
		const bool foundOk = GetPreparedObjectList(Plugin_, e, desired, handlers, false);
		if(!foundOk)
			return {};

		for(auto obj : handlers)
			if(auto idl = qobject_cast<IDownload*>(obj))
				return { obj, idl->AddJob(e) };

		return {};
	}

	bool EntityManager::CouldHandle(const Entity& e)
	{
		if(const auto res = EnsureUiThread([=, this] { return CouldHandle(e); }))
			return *res;

		const auto pm = Core::Instance().GetPluginManager();
		if(pm->GetInitStage() == PluginManager::InitStage::BeforeFirst)
		{
			qWarning() << Q_FUNC_INFO
					<< "called before first initialization stage is complete with"
					<< e.Entity_;
			qWarning() << e.Additional_;
			return false;
		}

		return !GetObjects(Plugin_, e).isEmpty();
	}

	bool EntityManager::HandleEntity(Entity e, QObject *desired)
	{
		if(const auto res = EnsureUiThread([=, this] { return HandleEntity(e, desired); }))
			return *res;

		if(!CheckInitStage(e, desired, HandleAfterInit_))
			return false;

		QObjectList handlers;
		const bool foundOk = GetPreparedObjectList(Plugin_, e, desired, handlers, true);
		if(!foundOk || handlers.isEmpty())
			return false;
		if(foundOk && handlers.isEmpty())
			return true;

		if(!(e.Parameters_ & TaskParameter::OnlyHandle))
			for(auto obj : handlers)
				if(auto idl = qobject_cast<IDownload*>(obj))
				{
					idl->AddJob(e);
					return true;
				}

		if(!(e.Parameters_ & TaskParameter::OnlyDownload))
		{
			for(auto obj : handlers)
				if(auto ieh = qobject_cast<IEntityHandler*>(obj))
					ieh->Handle(e);
			return !handlers.isEmpty();
		}

		return false;
	}

	QList<QObject*> EntityManager::GetPossibleHandlers(const Entity& e)
	{
		const auto pm = Core::Instance().GetPluginManager();
		if(pm->GetInitStage() == PluginManager::InitStage::BeforeFirst)
		{
			qWarning() << Q_FUNC_INFO
					<< "called before first initialization stage is complete with"
					<< e.Entity_;
			qWarning() << e.Additional_;
			return {};
		}

		return GetObjects(Plugin_, e);
	}

	template<typename F>
	std::optional<bool> EntityManager::EnsureUiThread(F&& cont)
	{
		if(QThread::currentThread() == thread())
			return {};

		bool res = false;
		QMetaObject::invokeMethod(this,
				cont,
				Qt::BlockingQueuedConnection,
				&res);
		return res;
	}

	namespace
	{
		constexpr auto AllowedStage = PluginManager::InitStage::BeforeSecond;
	}

	bool EntityManager::CheckInitStage(const Entity& e, QObject *desired, QVector<QueueEntry>& queue)
	{
		const auto pm = Core::Instance().GetPluginManager();
		if(pm->GetInitStage() >= AllowedStage)
			return true;

		qWarning() << Q_FUNC_INFO
				<< "got an entity handle request before first init is complete:"
				<< e.Entity_;
		qWarning() << e.Additional_;
		queue << QueueEntry { e, desired };

		if(DelegateAfterInit_.isEmpty() && HandleAfterInit_.isEmpty())
			connect(pm,
					&PluginManager::initStageChanged,
					this,
					&EntityManager::RunQueues);

		return false;
	}

	void EntityManager::RunQueues()
	{
		const auto pm = Core::Instance().GetPluginManager();
		if(pm->GetInitStage() != AllowedStage)
			return;

		disconnect(pm,
				&PluginManager::initStageChanged,
				this,
				&EntityManager::RunQueues);

		for(const auto& item : DelegateAfterInit_)
			DelegateEntity(item.Entity_, item.Desired_);
		for(const auto& item : HandleAfterInit_)
			HandleEntity(item.Entity_, item.Desired_);
		DelegateAfterInit_.clear();
		HandleAfterInit_.clear();
	}
}
